項目54 テンプレートリテラル型を使ってDSLや文字列間の関係をモデリングする
TypeScriptには文字列のパターンや関係を捉えるための独自ツールとして、テンプレートリテラル型がある
文字列リテラル型を使うと、文字列の有限集合をモデルングでき、string型は存在しうるすべての文字列の無限集合が含まれる
文字列リテラル型
type MedalColor = 'gold' | 'silver' | 'bronze';
テンプレートリテラル型でモデルングできるのは上記の間に位置するもの
たとえば、pseudoで始まるすべての文字の集合など
code:ts
type PseudoString = pseudo${string};
const science: PseudoString = 'pseudoscience'; // OK
const alias: PseudoString = 'pseudonym'; // OK
const physics: PseudoString = 'physics';
// ~~~~~~~ Type '"physics"' is not assignable to type 'pseudo${string}'.
// 型 '"physics"' を型 'pseudo${string}' に割り当てることはできません。
JavaScriptには構造化された文字列が多いので相性がいい
data-で始まるあらゆるプロパティを許容する型
code:ts
interface Checkbox {
id: string;
checked: boolean;
[key: data-${string}]: unknown;
}
テンプレートリテラル型は、stringの部分集合をモデル化するのに役立つが、一番はジェネリックや型推論と組み合わせて、型間の関係を表現する時
例:querySelector関数
引数に渡された、DOM要素の種類に応じて、具体的なElement型のサブタイプを返すようになっている
code:ts
const img = document.querySelector('img');
// ^? const img: HTMLImageElement | null
DOM要素と、ElementのサブタイプはHTMLElementTagNameMapで定義されている
code:ts
interface HTMLElementTagNameMap {
"a": HTMLAnchorElement;
"abbr": HTMLElement;
"address": HTMLElement;
"area": HTMLAreaElement;
// ... many more ...
"video": HTMLVideoElement;
"wbr": HTMLElement;
}
ただ、特定のIDを持つ画像を表現する文字列を引数に渡すと、サブタイプの参照がうまくいかずElementが返される
code:ts
const img = document.querySelector('img#spectacular-sunset');
// ^? const img: Element | null
💡解決策としては、テンプレートリテラル型を使ってオーバーロードを追加する
code:ts
type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
interface ParentNode {
querySelector<
TagName extends HTMLTag
(
selector: ${TagName}#${string}
): HTMLElementTagNameMapTagName | null;
}
}
const img = document.querySelector('img#spectacular-sunset');
// ^? const img: HTMLImageElement | null
ただ、不正確な型への一線を超えないように注意が必要
CSSセレクターのスペースは「~の子孫」を意味する
型の精度を高めようとして、項目40 正確でない型より精度の低い型を選択するの、「不正確さより精度の低さを選択せよ」に反している
div#container imgはIDがcontainerであるdiv要素の「子孫要素」である全てのimg要素を指すのに対し、HTMLDivElement型が返されている
code:ts
const img = document.querySelector('div#container img');
// ^? const img: HTMLDivElement | null
このような場合は、精度の低い型を得られるようにする(CSSセレクターの完全なパーサーを構築することもできる大変でしょ)
code:ts
type CSSSpecialChars = ' ' | '>' | '+' | '~' | '||' | ',';
type HTMLTag = keyof HTMLElementTagNameMap;
declare global {
interface ParentNode {
// 逃げ道
querySelector(
selector: ${HTMLTag}#${string}${CSSSpecialChars}${string}
): Element | null;
// 前と同じ
querySelector<
TagName extends HTMLTag
(
selector: ${TagName}#${string}
): HTMLElementTagNameMapTagName | null;
}
}
const img = document.querySelector('img#spectacular-sunset');
// ^? const img: HTMLImageElement | null
const img2 = document.querySelector('div#container img');
// ^? const img2: Element | null
CSSセレクターのようなドメイン固有言語(DSL)のパーサーを実装するために、条件型と組み合わされることがよくある
code:ts
type ToCamelOnce<S extends string> =
S extends ${infer Head}_${infer Tail} // inferでHeadで_の前後の文字列を取り出している
? ${Head}${Capitalize<Tail>}
: S;
type T = ToCamelOnce<'foo_bar'>; // 型は"fooBar"になる
foo_bar_bazのように複数_を持つ文字列に対応する場合は、再帰を使う
code:ts
type ToCamel<S extends string> =
S extends ${infer Head}_${infer Tail}
? ${Head}${Capitalize<ToCamel<Tail>>}
: S;
type T0 = ToCamel<'foo'>; // 型は"foo"
type T1 = ToCamel<'foo_bar'>; // 型は"fooBar"
type T2 = ToCamel<'foo_bar_baz'>; // 型は"fooBarBaz"
#TypeScript